/*
  Copyright 1996-2003
  Simon Whiteside

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

  $Id: TreeNode.java,v 1.11 2003/02/24 18:10:53 simkin_cvs Exp $
*/

package simkin;

import java.util.*;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.Writer;
import java.io.Reader;


/**
 * This class encapsulates a single node in a tree
 * <p>Each TreeNode consists of:
 * <ul><li>a text label<li>a textual value<li>a list of child nodes</ul>
 * Each element is optional. 
 * <p>A label is a piece of text containing letters, numbers and underscores, but no whitespace.
 * <p>The value can be any kind of text. 
 * <p>A treenode can be read and written to a text stream, and is represented as follows:
 * <pre>
 * Label [textual data] 
 * {
 *   ChildLabel
 * }
 *</pre>
 *<p>A TreeNode file contains an outermost set of braces:
 * <pre>
 * {
 *   Label [data]
 * }
 * </pre>
*/
final public class TreeNode
{
    String m_Label;
    String m_Data;
    Vector m_Items;
    static public final char g_FileMagic=0;
    static public final int g_FileVersion=1;
    static final public String g_true="true";
    static final public String g_false="false";

    /**
     * default constructor - blank label, data and an empty list of children
     */
    public TreeNode(){
	m_Label="";
	m_Data="";
	m_Items=null;
    }
    /**
     * Constructor - passing a label and data
     */
    public TreeNode(String label,String data){
	m_Label=label;
	m_Data=data;
	m_Items=null;
    }
    /**
     * Constructor - passing a label and data, which is converted to String
     */
    public TreeNode(String label,int data){
	m_Label=label;
	m_Data=new Integer(data).toString();
	m_Items=null;
    }
    /**
     * Constructor - passing a label and data, which is converted to String
     */
    public TreeNode(String label,float data){
	m_Label=label;
	m_Data= new Float(data).toString();
	m_Items=null;
    }
    /**
     * Constructor - passing a label and data, which is converted to String
     */
    public TreeNode(String label,boolean bool){
	m_Label=label;
	if (bool)
	    m_Data=g_true;
	else
	    m_Data=g_false;
	m_Items=null;
    }
    /**
     * Constructor - passing a label 
     */
    public TreeNode(String label){
	m_Label=label;
	m_Data="";
	m_Items=null;
    }
    /**
     * Adds a node to the list of this node's children
     */
    public void addChild(TreeNode child){
	checkItems();
	m_Items.addElement(child);
    }
    /**
     * Changes this node's label
     */
    public void label(String s){
	m_Label=s;
    }
    /**
     * Returns this node's label
     */
    public String label(){
	return m_Label;
    }
    /**
     * Returns this node's data
     */
    public String data(){
	return m_Data;
    }
    /**
     * Returns this node's data as an integer
     */
    public int intData(){
	int value=0;
	try{
	    value=new Integer(m_Data).intValue();
	}catch(Exception e){
	}
	return value;
    }
    /**
     * Returns this node's data as an float
     */
    public float floatData(){
	float value=0;
	try{
	    value=new Float(m_Data).floatValue();
	}catch(Exception e){
	}
	return value;
    }
    /**
     * sets this node's data
     */
    public void data(String s){
	m_Data=s;
    }
    /**
     * sets this node's integer data
     */
    public void intData(int i){
	m_Data=new Integer(i).toString();
    }
    /**
     * sets this node's float data
     */
    public void floatData(float i){
	m_Data=new Float(i).toString();
    }
    /**
     * sets this node's boolean data
     */
    public void intData(boolean bool){
	if (bool)
	    m_Data=g_true;
	else
	    m_Data=g_false;
    }

    /**
     * finds the first child with the given label - returns null if none found
     */
    public TreeNode findChild(String s){
	TreeNode child=null;
	if (m_Items!=null && m_Items.size()>0){
	    for (int i=0;i<m_Items.size();i++){
		TreeNode this_child=(TreeNode)m_Items.elementAt(i);
		if (this_child.label().equals(s)){
		    child=this_child;
		    break;
		}
	    }
	}
	return child;
    }
    /**
     * finds the first child with the given label and data - returns null if none found
     */
    public TreeNode findChild(String label,String data){
	TreeNode child=null;
	if (m_Items!=null && m_Items.size()>0){
	    for (int i=0;i<m_Items.size();i++){
		TreeNode this_child=(TreeNode)m_Items.elementAt(i);
		if (this_child.label().equals(label) && this_child.data().equals(data)){
		    child=this_child;
		    break;
		}
	    }
	}
	return child;
    }
    /**
     * returns the data of the first child with the given label
     * @param label - the label to search for
     * @param def - a value to return if no child is found
     */
    public String findChildData(String label,String def){
	String ret=def;
	TreeNode child=findChild(label);
	if (child!=null)
	    ret=child.data();
	return ret;
    }
    /**
     * returns the data of the first child with the given label
     * @param label - the label to search for
     */
    public String findChildData(String s){
	String ret="";
	TreeNode child=findChild(s);
	if (child!=null)
	    ret=child.data();
	return ret;
    }
    /**
     * returns the data of the first child with the given label as an integer
     * @param label - the label to search for
     */
    public int findChildIntData(String s){
	TreeNode child=findChild(s);
	int value=0;
	if (child!=null)
	    value=child.intData();
	return value;
    }
    /**
     * returns the data of the first child with the given label as a float
     * @param label - the label to search for
     */
    public float findChildFloatData(String s){
	TreeNode child=findChild(s);
	float value=0;
	if (child!=null)
	    value=child.floatData();
	return value;
    }
    /**
     * Returns this node's data as a boolean
     */
    public boolean boolData(){
	boolean bRet=false;
	if (m_Data.equals("true"))
	    bRet=true;
	return bRet;
    }
    /**
     * returns the data of the first child with the given label as a boolean
     * @param label - the label to search for
     */
    public boolean findChildBoolData(String s){
	boolean ret=false;
	TreeNode child=findChild(s);
	if (child!=null)
	    ret=child.boolData();
	return ret;
    }
    /**
     * replaces the first child with a matching label with the contents of the given node. If no match is found, the node is added at the end of the list of children
     */
    public void setChild(TreeNode node){
	TreeNode child=null;
	boolean replaced=false;
	String s=node.label();
	if (m_Items!=null && m_Items.size()>0){
	    for (int i=0;i<m_Items.size();i++){
		TreeNode this_child=(TreeNode)m_Items.elementAt(i);
		if (this_child.label().equals(s)){
		    m_Items.setElementAt(node,i);
		    replaced=true;
		    break;
		}
	    }
	}
	if (replaced==false)
	    addChild(node);
    }
    /**
     * returns the data of the first child with the given label as a boolean
     * @param label - the label to search for
     * @param def - the default value if the child is not found
     */
    public boolean findChildBoolData(String label,boolean def){
	boolean ret=def;
	String data=findChildData(label);
	if (data.equals("true"))
	    ret=true;
	return ret;
    }
    /**
     * returns the data of the first child with the given label as a boolean
     * @param label - the label to search for
     * @param def - the default value if the child is not found
     */
    public int findChildIntData(String s,int def){
	int ret=def;
	TreeNode child=findChild(s);
	if (child!=null)
	    ret=child.intData();
	return ret;
    }
    /**
     * This function attempts to read a TreeNode from the given stream. If there is a parse error, an IOException is thrown
     */
    public static TreeNode read(InputStream in) throws java.io.IOException{
	TreeNodeReader reader=new TreeNodeReader(in);
	return reader.read();
    }
    /**
     * This function attempts to read a TreeNode from the given reader. If there is a parse error, an IOException is thrown
     */
    public static TreeNode read(Reader in) throws java.io.IOException{
	TreeNodeReader reader=new TreeNodeReader(in);
	return reader.read();
    }
    /**
     * This method writes the current node out to a writer
     * @param out - the writer to write to
     * @param include_tabs - include tabs in a non-compiled output to make appearance more attractive
     */
    public void write(Writer out,boolean include_tabs) throws java.io.IOException{
	write(out,0,include_tabs);
	out.flush();
    }
    /**
     * This method writes the current node out to an output stream. 
     * @param out - the stream to write to
     * @param compiled - if set to false, the output is textual, otherwise it is a compressed binary format
     * @param include_tabs - include tabs in a non-compiled output to make appearance more attractive
     */
    public void write(OutputStream out,boolean compiled,boolean include_tabs) throws java.io.IOException{
	if (compiled){
	    out.write(g_FileMagic);
	    out.write(g_FileVersion);
	    Hashtable stringTable=new Hashtable();
	    write(out,stringTable);
	}else
	    write(out,0,include_tabs);
    }
    /**
     * This method writes an integer out to a binary stream
     */
    void writeInt(OutputStream out,int i) throws java.io.IOException{
	out.write(i & 0xFF);
	i/=0x100;
	out.write(i);
    }
    /**
     * This method writes a hashtable of strings out to a binary stream
     */
    void write(OutputStream out, Hashtable stringTable) throws java.io.IOException{
	if (m_Label.length()==0){
	    writeInt(out,0);
	    writeInt(out,0);
	}else{
	    Integer index=(Integer)(stringTable.get(m_Label));
	    if (index==null){
		stringTable.put(m_Label,new Integer(stringTable.size()));
		writeInt(out,m_Label.length());
		out.write(m_Label.getBytes(),0,m_Label.length());
	    }else{
		writeInt(out,0);
		writeInt(out,index.intValue()+1);
	    }
	}
	writeInt(out,m_Data.length());
	if (m_Data.length()>0)
	    out.write(m_Data.getBytes(),0,m_Data.length());
	writeInt(out,numChildren());
	if (m_Items!=null)
	    for (int i=0;i<m_Items.size();i++)
		((TreeNode)m_Items.elementAt(i)).write(out,stringTable);
    }
    /**
     * this function returns the list of children of this node
     */
    public Vector getItems(){
	return m_Items;
    }
    /**
     * This function writes a string to a text stream with the given indentation level
     * @param include_tabs - include tabs in a non-compiled output to make appearance more attractive
     */
    void writeString(OutputStream out,String str,int tabs,boolean include_tabs) throws java.io.IOException{
	if (str.length()>0){
	    if (include_tabs)
		for (int i=0;i<tabs;i++)
		    out.write('\t');
	    for (int i=0;i<str.length();i++)
		out.write(str.charAt(i));
	}
    }
    /**
     * This function writes a string to a writer with the given indentation level
     * @param include_tabs - include tabs in a non-compiled output to make appearance more attractive
     */
    void writeString(Writer out,String str,int tabs,boolean include_tabs) throws java.io.IOException{
	if (str.length()>0){
	    if (include_tabs)
		for (int i=0;i<tabs;i++)
		    out.write('\t');
	    out.write(str);
	}
    }
    /**
     * This method returns a version of the given string with any occurrences of "[" or "]" escaped with \
     */
    static String escapeData(String original){
	String replaced=original;
	if (original.indexOf("[")>=0 || original.indexOf("]")>=0){
	  replaced="";
	  StringTokenizer tokenizer=new StringTokenizer(original,"[]",true);
	  while (tokenizer.hasMoreTokens()){
	    String token=tokenizer.nextToken();
	    if (token.equals("["))
	      token="\\[";
	    else
	      if (token.equals("]"))
		token="\\]";
	    replaced+=token;
	  }
	}
	return replaced;
    }
    /**
     * This method writes out the current node to a text stream with the given indentation level
     * @param include_tabs - include tabs in a non-compiled output to make appearance more attractive
     */
    void write(OutputStream out,int tabs,boolean include_tabs) throws java.io.IOException{
	writeString(out,m_Label,tabs,include_tabs);
	if (m_Data.length()>0){
	    String data=escapeData(m_Data);
	    writeString(out,Utils.addStrings(" [",data,"]"),0,include_tabs);
	    writeString(out,"\n",0,include_tabs);
	}else if (m_Label.length()>0)
	    writeString(out,"\n",0,include_tabs);
	if (m_Items!=null && m_Items.size()>0){
	    writeString(out,"{\n",tabs,include_tabs);
	    for (int i=0;i<m_Items.size();i++){
		TreeNode child=(TreeNode)m_Items.elementAt(i);
		child.write(out,tabs+1,include_tabs);
	    }
	    writeString(out,"}\n",tabs,include_tabs);
	}
    }
    /**
     * This method writes out the current node to a writer with the given indentation level
     * @param include_tabs - include tabs in a non-compiled output to make appearance more attractive
     */
    void write(Writer out,int tabs,boolean include_tabs) throws java.io.IOException{
	writeString(out,m_Label,tabs,include_tabs);
	if (m_Data.length()>0){
	    String data=escapeData(m_Data);
	    writeString(out,Utils.addStrings(" [",data,"]"),0,include_tabs);
	    writeString(out,"\n",0,include_tabs);
	}else if (m_Label.length()>0)
	    writeString(out,"\n",0,include_tabs);
	if (m_Items!=null && m_Items.size()>0){
	    writeString(out,"{\n",tabs,include_tabs);
	    for (int i=0;i<m_Items.size();i++){
		TreeNode child=(TreeNode)m_Items.elementAt(i);
		child.write(out,tabs+1,include_tabs);
	    }
	    writeString(out,"}\n",tabs,include_tabs);
	}
    }
    /**
     * this method creates the list of children, if it does not already exist
     */
    void checkItems(){
	if (m_Items==null)
	    m_Items=new Vector();
    }
    /**
     * This method returns the number of children in the list at this node
     */
    public int numChildren(){
	int num=0;
	if (m_Items!=null)
	    num=m_Items.size();
	return num;
    }
    /**
     * This method returns the nth child at this node
     */
    public TreeNode nthChild(int index){
	TreeNode child=null;
	if (m_Items!=null)
	    child=(TreeNode)m_Items.elementAt(index);
	return child;
    }
    /**
     * this method returns the data of the nth child as an integer
     * @param index - the index of the child
     * @param defaultData - the value to return if there are no children
     */
    public int nthChildIntData(int index,int defaultData){
	int data=defaultData;
	if (m_Items!=null){
	    TreeNode child=(TreeNode)m_Items.elementAt(index);
	    if (child!=null)
		data=child.intData();
	}
	return data;
    }
    /**
     * this method returns the data of the nth child as a float
     * @param index - the index of the child
     * @param defaultData - the value to return if there are no children
     */
    public float nthChildFloatData(int index,float defaultData){
	float data=defaultData;
	if (m_Items!=null){
	    TreeNode child=(TreeNode)m_Items.elementAt(index);
	    if (child!=null)
		data=child.floatData();
	}
	return data;
    }
    /**
     * this method returns the data of the nth child 
     * @param index - the index of the child
     * @param defaultData - the value to return if there are no children
     */
    public String nthChildData(int index){
	String data="";
	if (m_Items!=null){
	    TreeNode child=(TreeNode)m_Items.elementAt(index);
	    if (child!=null)
		data=child.data();
	}
	return data;
    }
    /** this deletes any children of this node */
    public void clear(){
	if (m_Items!=null)
	    m_Items.removeAllElements();
    }
    /**
     * This function removes the given node from the list of children
     */
    public void removeChild(TreeNode node){
	if (m_Items!=null)
	    m_Items.removeElement(node);
    }
    /**
     * This method returns an iterator that can be used to iterate over the children of this node
     */
    public TreeNodeIterator getIterator(){
	return new TreeNodeIterator(this);
    }
    /**
     * this method returns a cloned version of this node - it performs a deep copy
     */
    public Object clone(){
	TreeNode newNode=new TreeNode(m_Label,m_Data);
	if (m_Items!=null){
	    newNode.m_Items=new Vector();
	    for (int i=0;i<m_Items.size();i++){
		TreeNode node=(TreeNode)m_Items.elementAt(i);
		newNode.m_Items.addElement(node.clone());
	    }
	}
	return newNode;
    }
    /**
     * this function moves the children from the given node into this node
     */
    public void moveItemsFrom(TreeNode node){
	m_Items=node.m_Items;
	node.m_Items=null;
    }
    /**
     * this function performs a deep copy from the children of another node into this one
     */
    public void copyItems(TreeNode node){
    //	This function copies the items from the node into our list (taking copies)
	m_Items=null;
	if (node.m_Items!=null){
	    m_Items=new Vector();
	    for (int i=0;i<node.m_Items.size();i++){
		TreeNode this_child=(TreeNode)(node.m_Items.elementAt(i));
		m_Items.addElement(this_child.clone());
	    }
	}
    }
    /**
     * This method returns an object which can iterate over all the immediate child node of this node
     */
    public TreeNodeEnumerator enumerate(){
	return new TreeNodeEnumerator(this);
    }
}




